﻿using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
namespace Sparklings.RayTracing
{
    public class Ray
    {
        public Vector2 Origin;
        public Vector2 Direction;
        /// <summary>
        /// Tells wether the ray could hit the segment, if the line of sight was clear.
        /// </summary>
        /// <param name="s">The segment to test.</param>
        /// <returns>null if no collision, else the point where the ray hits the segment.</returns>
        public Vector2? Collide(LightHittable s)
        {
            // Paralel underlying lines case.

            float x1 = Origin.X, y1 = Origin.Y;
            float x2 = Origin.X + Direction.X, y2 = Origin.Y + Direction.Y;
            float x3 = s.Origin.X, y3 = s.Origin.Y;
            float x4 = s.End.X, y4 = s.End.Y;
            float divider = ((x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4));
            if (divider < 0.001f && divider > -0.001f)
            {
                return null;
            }
            // Now we have an intersection. But is it on the ray and the segment ?
            Vector2 intersection =
                new Vector2(
                    ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) /
                        divider,
                    ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) /
                        divider);
            // Half-line.
            if (!sameWay((intersection - Origin), Direction))
            {
                return null;
            }
            if (!sameWay((intersection - s.Origin), s.Direction)
                || (intersection - s.Origin).LengthSquared() > (s.End - s.Origin).LengthSquared())
            {
                return null;
            }
            return intersection;
        }

        /// <summary>
        /// Finds out which of the given segments are hit by the ray, taking in account occlusion.
        /// </summary>
        /// <param name="segmentsToTest">The segments to test.</param>
        /// <returns>The segments hit. Will contain at most one occluding segment, 
        /// and any number of non occluding segments.</returns>
        public IList<LightHit> Collide(IList<LightHittable> segmentsToTest)
        {
            List<LightHittable> occluders = new List<LightHittable>();
            List<LightHittable> nonOccluders = new List<LightHittable>();
            foreach (LightHittable s in segmentsToTest)
            {
                if (s.IsOccluder)
                {
                    occluders.Add(s);
                }
                else
                {
                    nonOccluders.Add(s);
                }
            }
            float closerOccluderDist2 = -1;
            LightHit? occluder = null;
            foreach (LightHittable s in occluders)
            {
                Vector2? curCollisionPosition = Collide(s);
                if (curCollisionPosition != null)
                {
                    float curDist2 = (curCollisionPosition.Value - Origin).LengthSquared();
                    if (occluder == null || curDist2 < closerOccluderDist2)
                    {
                        closerOccluderDist2 = curDist2;
                        occluder = new LightHit(s, curCollisionPosition.Value);
                    }
                }
            }
            List<LightHit> ret = new List<LightHit>();
            if (occluder != null) { ret.Add(occluder.Value); }
            foreach (LightHittable s in nonOccluders)
            {
                Vector2? curCollisionPosition = Collide(s);
                if (curCollisionPosition != null)
                {
                    float curDist2 = (curCollisionPosition.Value - Origin).LengthSquared();
                    if (occluder == null || curDist2 < closerOccluderDist2)
                    {
                        ret.Add(new LightHit(s, curCollisionPosition.Value));
                    }
                }
            }
            return ret;
        }

        /// <summary>
        /// Tells wether two collinear vectors are facing the same direction.
        /// </summary>
        /// <param name="v1">A non-zero vector.</param>
        /// <param name="v2">A non-zero vector, collinear (more or less) to the first one.</param>
        /// <returns>true if both vectors are facing the same direction.</returns>
        private bool sameWay(Vector2 v1, Vector2 v2)
        {
            return Math.Sign(v1.X) == Math.Sign(v2.X) && Math.Sign(v1.Y) == Math.Sign(v2.Y);
        }
    }
}
